mTLSを使ったEnclave間の通信 | Anonify解体新書5
前回の内容はこちら
お知らせ
ついにAnonifyの本番運用が始まりました
TEE + ブロックチェーンのシステムが本番稼働しているのは世界的にみても珍しいかも
期間は2021年9月21日から10月11日までです
アンケートの裏側でAnonifyが動いてます
プレスリリースと関連ニュース
TL;DR
セキュリティ・プライバシー保護技術Anonifyの主要技術要素について解説する連載記事(全8回)
Anonifyで使用されている技術要素を洗い出し重要な技術について簡単なサンプルプログラムを交えて解説する。
その5では「mTLSを使ったEnclave間の通信」と題して、mutual-TLS(mTLS)を使って複数のEnclaveアプリケーション間でデータ通信をする方法について解説する
サンプルプログラムはRustで開発
データのシーリングにはマシンごとにシーリングキーが異なるという重大な制約がある
SGXマシンを1台しか使わないという制約があれば問題ない
現実的にはマシン1台で完結するようなサービスはあまりない
SGXマシンを複数使用してE2EEを実現するためには複数のSGXマシンに配置されたEnclaveアプリケーションが同じプライベートキーを共有する必要がある
Enclaveアプリケーション同士がmutual-TLSを使って通信をするば安全にプライベートキーを共有できる
ここで使用するコードはすべて独立して動作するのでAnonify自体の知識やAnonifyの動作環境は不要
この記事の中で使用する図は特別な記載がない限り全て筆者が作成したもの
サンプルプログラムはこちら
Anonifyが使用している主な技術要素
TEE(Intel SGX)関連
OCall/ECall
Remote Attestation
crypto_box(NaCl)
データのシーリング
mutual-TLS <- 今回解説するのはここ
Blockchain関連
スマートコントラクト
Web3
E2EEのプライベート鍵の管理はデータのシーリングで解決できるのか
複数のSGXマシンでプライベート鍵を共有する場合、データのシーリングが使えない
CPUごとにシーリングキーが異なるためMRENCLAVEの値が同じであってもマシンが違うとアンシーリングできない(PlantUML) https://scrapbox.io/files/614c8def9e43a50023372779.svg
E2EEのプライベート鍵を複数のEnclaveアプリで共有するにはどうすれば良いか
複数のSGXマシン上にEnclaveアプリをスケールさせるにはデータのシーリング以外の方法を使わないといけない
mutual-TLSでEnclave同士で通信をしてデータを共有する
Enclaveアプリ間の通信にmutual-TLSを使う
mutual-TLS(mTLS)
mutualは双方とか、お互いという意味
通常のTLS接続はクライアントがサーバの証明書を認証する
mTLSはクライアントがサーバの証明書を認証するのに加えサーバ側でクライアント証明書の認証を実施する
サーバとクライアントが互いに証明書を提示し合い、互いを認証し合う
同じクラスタ内のEnclaveアプリ同士のローカルな通信において、サーバ/クライアント証明書をどうするか
お互いが正当なEnclaveアプリであることを証明したい
Remoto Attestationで得られるAttestationレポートからX.509証明書を生成しECDSA署名をする
この証明書をクライアント/サーバ証明書として使う
TCPのストリーム処理はEnclaveの中で行う
非Enclave側ではTCPリスナーの処理(サーバ)やTCPストリームの接続処理(クライアント)のみ実施
AnonifyでもEnclave同士のmTLS通信をして鍵データを共有している
TLS接続についてはこちらの書籍がわかりやすくておすすめ
cipepser.icon みやたひろしさんのネットワーク設計の本も大変わかりやすくておすすめです。
ふたたびのRemote Attestation
Remote Attestationの詳細は以下を参照のこと
mTLS通信で使用する証明書に、Remote Attestationで得られるAttestation Reportを利用する
サーバ/クライアントお互いがEnclaveの検証をすることができる(PlantUML) https://scrapbox.io/files/615434c5cb8c9b001d595fa7.svg
Enclaveアプリのビルド時に通信相手のMRENCLAVEの値をそれぞれ交換すればより確実な認証が可能
プログラムのアップデートが発生すると通信相手ごと再ビルドしないとダメなのが若干悩ましい
MRENCLAVEだとローリングアップデートはほぼ無理
Anonifyではビルド時に通信相手のMRENCLAVE値を渡してmTLS通信時に検証する仕組みがある
MRSIGNERだとそこまで気にしなくても良さそう
気にするのは鍵のローテンションの時くらいでよさそう
mTLS通信の証明書にAttestationレポートを使うアイデアはこちらのホワイトペーパーが元ネタっぽい
大規模にSGXマシンをスケールさせるためインフラ構成
Kubernetesを想定
Azure Kubernetes Service(AKS)はノードにSGXマシンを使用することができる
コンフィデンシャル コンピューティング アドオン
AnonifyもAKSを使って本番運用している
AKSでEnclaveアプリを構築するインフラアーキテクチャ案
Nodeごとに異なるSGXマシンが割り当てられる
1台のEnclaveアプリが生成した乱数を複数のEnclaveアプリが共有して各Enclaveアプリで同じプライベートキーを生成仕組み
ランダム生成EnclaveアプリとサーバEnclaveアプリに分ける(PlantUML) ランダム生成Enclaveアプリはスケールさせない、サーバEnclaveアプリをスケールさせる
ランダム生成Enclaveアプリは起動時にランダムを生成してデータをシーリングして保持しておく
https://scrapbox.io/files/6151a0012153b70020ea4d79.svg
mTLS + RAを使った通信プログラムをかいてみる
クライアントからサーバにmTLS接続する
クライアントからhelloと送信するとサーバがhello backと返す
データの読み書きはEnclaveの中だけで行う
サーバ
Remote Attestationレポートからサーバ証明書を生成する
接続時にクライアント証明書の認証をする
クライアント
Remote Attestationレポートからクライアント証明書を生成する
接続時にサーバ証明書の認証をする
リモートアテステーション
Remote Attestationの処理はサーバ/クライアントともに共通なのでクレート化する
サンプルプログラムの構成
code: mutual-ra
mutual-ra/ # サンプルプログラムのディレクトリ
└ server
├ Makefile
├ app/
├ enclave/
└ lib/
└ client
├ Makefile
├ app/
├ enclave/
└ lib/
└ attestation
├ AttestationReportSigningCACert.pem # RA検証用PEMファイル
├ Cargo.toml
├ attestation.edl # RA共通EDL
└ src
プログラムの全体像
今回はサーバとクライアントのプログラムがあるので少し複雑
サーバ/クライアント共通でRAの検証が必要なので一部共通処理をクレート化した
Attestationクレート
https://scrapbox.io/files/615485733c24be001d4bfd64.svg
今回使用するライブラリ
TcpListener/TcpStream
TCP接続するためのRust標準ライブラリ
RustのTLSライブラリ
TLS 1.3
デジタル署名を検証するライブラリ
今回はRAを使った独自検証になるので、検証の機能は仕様しない
エラーのEnumを使ったりDNS名の参照機能を使うくらい
ASN.1のライブラリ
AttestationレポートからECDSA (Elliptic Curve Digital Signature Algorithm)の電子署名が施されたX.509証明書を生成する
ECDSAの電子署名にはASN.1 DERフォーマットのエンコードが必要
Rustでシステムプログラミングするためのライブラリ
Graceful Shutdown時のPoll処理で使用した
nixは*nix、つまりいろんなUnix派生OSという意味らしい
この記事の中ではTcpListener/TcpStreamとRustlsを使ったコードを中心に紹介する
Attestationプログラム
基本的にはAnonify解体新書2で紹介した処理を独立させたもの
サーバプログラム、クライアントプログラムが使用するクレート
IASからAttestationレポートを取得する処理
Anonify解体新書2とほぼ同じ
ECDSAの機能(新規追加)
Attestationレポートを証明書にするための処理
Rust SGX SDKのサンプルとほぼ同じ処理
クライアント証明書の認証処理のための構造体(新規追加)
クライアント証明書が信頼できるかサーバ側で検証する
クライアントのAttestationレポートの検証処理
rustlsのClientCertVerifierトレイトを拡張する
検証処理の内容はAnonify解体新書2のverify_ra_cert関数とほぼ同じ
code: verifier.rs
pub struct ClientVerifier {
// SGX_ERROR_UPDATE_NEEDEDを無視するかどうか
outdated_ok: bool,
}
impl ClientVerifier {
pub fn new(outdated_ok: bool) -> ClientVerifier {
ClientVerifier {
outdated_ok: outdated_ok,
}
}
}
impl rustls::ClientCertVerifier for ClientVerifier {
fn client_auth_root_subjects(
&self,
_sni: Option<&webpki::DNSName>,
) -> Option<rustls::DistinguishedNames> {
Some(rustls::DistinguishedNames::new())
}
fn verify_client_cert(
&self,
_sni: Option<&webpki::DNSName>,
) -> Result<rustls::ClientCertVerified, rustls::TLSError> {
// Attestationレポートを検証する関数を呼び出す
match verification::verify_ra_cert(&certs0.0) { Ok(()) => Ok(rustls::ClientCertVerified::assertion()),
Err(sgx_status_t::SGX_ERROR_UPDATE_NEEDED) => {
... エラー処理は省略 ...
}
Err(_) => Err(rustls::TLSError::WebPKIError(
webpki::Error::ExtensionValueInvalid,
)),
}
}
}
サーバ証明書の認証処理のための構造体(新規追加)
サーバ証明書が信頼できるかクライアント側で検証する
サーバのAttestationレポートの検証処理
rustlsのServerCertVerifierトレイトを拡張する
検証処理の内容はAnonify解体新書2のverify_ra_cert関数とほぼ同じ
code: verifier.rs
pub struct ServerVerifier {
// SGX_ERROR_UPDATE_NEEDEDを無視するかどうか
outdated_ok: bool,
}
impl ServerVerifier {
pub fn new(outdated_ok: bool) -> ServerVerifier {
ServerVerifier {
outdated_ok: outdated_ok,
}
}
}
impl rustls::ServerCertVerifier for ServerVerifier {
fn verify_server_cert(
&self,
_roots: &rustls::RootCertStore,
_hostname: webpki::DNSNameRef,
) -> Result<rustls::ServerCertVerified, rustls::TLSError> {
// Attestationレポートを検証する関数を呼び出す
match verification::verify_ra_cert(&certs0.0) { Ok(()) => Ok(rustls::ServerCertVerified::assertion()),
Err(sgx_status_t::SGX_ERROR_UPDATE_NEEDED) => {
... エラー処理は省略 ...
}
Err(_) => Err(rustls::TLSError::WebPKIError(
webpki::Error::ExtensionValueInvalid,
)),
}
}
}
サーバプログラム
TcpListenerでクライアントからの接続を待ち受ける
Graceful Shutdownに対応
Attestationレポートからサーバ証明書を生成してクライアントに渡す
クライアントから送られてきたクライアント証明書を検証する
クライアントから受け取ったデータを標準出力に書き出してからクライアントにレスポンスを返す
サーバAppのプログラム
TCPリスナーを生成してクライアントからの接続を待ち受ける
code: main.rs
use std::net::TcpListener;
use std::os::unix::io::AsRawFd;
... OCallの処理は省略 ...
extern "C" {
fn run_server_session(
eid: sgx_enclave_id_t,
retval: *mut sgx_status_t,
socket_fd: c_int,
sign_type: sgx_quote_sign_type_t,
) -> sgx_status_t;
}
// Enclaveの初期化処理
fn init_enclave() -> SgxResult<SgxEnclave> {
... 処理は省略 ...
}
fn main() {
// Enclaveを初期化する
let enclave = match init_enclave() {
... エラー処理は省略 ...
};
println!("Running as server...");
// TcpListenerオブジェクトの生成
// EventFdとCancellableIncoming構造体はGraceful Shutdownのためのクレート
let shutdown = EventFd::new();
let listener = TcpListener::bind("0.0.0.0:3443").unwrap();
let incoming = CancellableIncoming::new(&listener, &shutdown);
// クライアントからの接続を待ち受ける
for stream in incoming {
match stream {
Ok(socket) => {
println!("connects new client");
let mut retval = sgx_status_t::SGX_SUCCESS;
let sign_type = sgx_quote_sign_type_t::SGX_LINKABLE_SIGNATURE;
// Enclaveプログラムの呼び出し
let result = unsafe {
run_server_session(enclave.geteid(), &mut retval, socket.as_raw_fd(), sign_type)
};
... エラー処理は省略 ...
}
Err(e) => panic!("Unexpected error: {}", e),
}
}
shutdown.add(1);
enclave.destroy();
}
サーバEnclaveのプログラム
キーペアを生成する
証明書生成に必要
IASからAttestationレポートを取得する
サーバ証明書(ECDSA署名)を生成する
コンフィグの設定
ClientVerifier(クライアント証明書の認証)オブジェクトをセットする
サーバ証明書をコンフィグにセットする
TLSサーバセッションの生成
クライアントから受け取ったデータを読み込んで標準出力に書き出す
クライアントにレスポンスを返す
code: lib.rs
pub extern "C" fn run_server_session(
socket_fd: c_int,
sign_type: sgx_quote_sign_type_t,
) -> sgx_status_t {
let ias_key = env::var("IAS_KEY").expect("IAS_KEY is not set");
let spid_env = env::var("SPID").expect("SPID is not set");
let spid = decode_spid(&spid_env);
// キーペアを生成する
let ecc_handle = SgxEccHandle::new();
let _result = ecc_handle.open();
let (prv_k, pub_k) = ecc_handle.create_key_pair().unwrap();
// IASからAttestationレポートを取得する
let (attn_report, sig, cert) =
match attestation_report::create(ias_key, spid, &pub_k, sign_type) {
... エラー処理は省略 ...
};
// サーバ証明書(ECDSA署名)を生成する
let payload = attn_report + "|" + &sig + "|" + &cert;
let (key_der, cert_der) = match ecdsa::gen_ecc_cert(payload, &prv_k, &pub_k, &ecc_handle) {
... エラー処理は省略 ...
};
let _result = ecc_handle.close();
// ClientVerifier(クライアント証明書の認証)オブジェクトを引数にセットしてサーバコンフィグを生成する
let mut cfg = rustls::ServerConfig::new(Arc::new(ClientVerifier::new(true)));
// サーバ証明書をコンフィグにセットする
let mut certs = Vec::new();
certs.push(rustls::Certificate(cert_der));
let privkey = rustls::PrivateKey(key_der);
cfg.set_single_cert_with_ocsp_and_sct(certs, privkey, vec![], vec![])
.unwrap();
// TLSサーバセッションの生成
let mut sess = rustls::ServerSession::new(&Arc::new(cfg));
let mut conn = TcpStream::new(socket_fd).unwrap();
// クライアントから受け取ったデータを読み込んで標準出力に書き出す
let mut tls = rustls::Stream::new(&mut sess, &mut conn);
match tls.read(&mut plaintext) {
Ok(_) => println!("Client said: {}", str::from_utf8(&plaintext).unwrap()),
Err(e) => {
println!("Error in read_to_end: {:?}", e);
panic!("");
}
};
// クライアントにレスポンスを返す
tls.write("hello back".as_bytes()).unwrap();
sgx_status_t::SGX_SUCCESS
}
クライアントプログラム
TcpStreamでサーバに接続する
Attestationレポートからクライアント証明書を生成してサーバからの要求に応じて証明書を渡す
サーバから送られてきたサーバ証明書を検証する
クライアントAppのプログラム
TCP接続する
code: main.rs
... OCallの処理は省略 ...
extern "C" {
fn run_client_session(
eid: sgx_enclave_id_t,
retval: *mut sgx_status_t,
socket_fd: c_int,
sign_type: sgx_quote_sign_type_t,
) -> sgx_status_t;
}
// Enclaveの初期化処理
fn init_enclave() -> SgxResult<SgxEnclave> {
... 処理は省略 ...
}
fn main() {
let enclave = match init_enclave() {
... エラー処理は省略 ...
};
println!("Running as client...");
// TCP接続する
let socket = TcpStream::connect("localhost:3443").unwrap();
let mut retval = sgx_status_t::SGX_SUCCESS;
let sign_type = sgx_quote_sign_type_t::SGX_LINKABLE_SIGNATURE;
// Enclaveプログラムの呼び出し
let result =
unsafe { run_client_session(enclave.geteid(), &mut retval, socket.as_raw_fd(), sign_type) };
... エラー処理は省略 ...
enclave.destroy();
}
クライアントEnclaveのプログラム
キーペアを生成する
証明書生成に必要
IASからAttestationレポートを取得する
クライアント証明書(ECDSA署名)を生成する
コンフィグの設定
クライアント証明書をセットする
ServerVerifierオブジェクトをセットする
TLSクライアントセッションの生成
クライアントからサーバにデータを送信する
サーバからのレスポンスを標準出力に書き込む
code: lib.rs
pub extern "C" fn run_client_session(
socket_fd: c_int,
sign_type: sgx_quote_sign_type_t,
) -> sgx_status_t {
let ias_key = env::var("IAS_KEY").expect("IAS_KEY is not set");
let spid_env = env::var("SPID").expect("SPID is not set");
let spid = decode_spid(&spid_env);
// キーペアを生成する
let ecc_handle = SgxEccHandle::new();
let _result = ecc_handle.open();
let (prv_k, pub_k) = ecc_handle.create_key_pair().unwrap();
// IASからAttestationレポートを取得する
let (attn_report, sig, cert) =
match attestation_report::create(ias_key, spid, &pub_k, sign_type) {
... エラー処理は省略 ...
};
// クライアント証明書(ECDSA署名)を生成する
let payload = attn_report + "|" + &sig + "|" + &cert;
let (key_der, cert_der) = match ecdsa::gen_ecc_cert(payload, &prv_k, &pub_k, &ecc_handle) {
... エラー処理は省略 ...
};
let _result = ecc_handle.close();
// クライアント証明書をコンフィグにセットする
let mut cfg = rustls::ClientConfig::new();
let mut certs = Vec::new();
certs.push(rustls::Certificate(cert_der));
let privkey = rustls::PrivateKey(key_der);
cfg.set_single_client_cert(certs, privkey).unwrap();
// ServerVerifier(サーバ証明書を認証するための構造体)オブジェクトをコンフィグにセットする
cfg.dangerous()
.set_certificate_verifier(Arc::new(ServerVerifier::new(true)));
cfg.versions.clear();
cfg.versions.push(rustls::ProtocolVersion::TLSv1_3);
// TLSクライアントセッションの生成
let dns_name = webpki::DNSNameRef::try_from_ascii_str("localhost").unwrap();
let mut sess = rustls::ClientSession::new(&Arc::new(cfg), dns_name);
let mut conn = TcpStream::new(socket_fd).unwrap();
// クライアントからサーバにデータを送信する
let mut tls = rustls::Stream::new(&mut sess, &mut conn);
tls.write("hello".as_bytes()).unwrap();
// サーバからのレスポンスを標準出力に書き込む
let mut plaintext = Vec::new();
match tls.read_to_end(&mut plaintext) {
Ok(_) => {
println!("Server replied: {}", str::from_utf8(&plaintext).unwrap());
}
Err(ref err) if err.kind() == io::ErrorKind::ConnectionAborted => {
println!("EOF (tls)");
}
Err(e) => println!("Error in read_to_end: {:?}", e),
}
sgx_status_t::SGX_SUCCESS
}
実行結果
サーバ
code: bash
+ Init Enclave Successful 2! Running as server...
connects new client
Entering ocall_sgx_init_quote
get_sigrl_from_intel
Report creation => success 131, 215, 25, 231, 125, 234, 202, 20, 112, 246, 186, 246, 42, 77, 119, 67, 3, 200, 153, 219, 105, 2, 15, 156, 112, 238, 29, 252, 8, 199, 206, 158 rand finished
Entering ocall_get_quote
quote size = 1116
sgx_calc_quote_size returned SGX_SUCCESS.
rsgx_verify_report passed!
post_report_from_intel
Time = 2021-09-30T12:45:29.702860+0000
rt=SGX_ERROR_UPDATE_NEEDED
update_info.pswUpdate: 1
update_info.csmeFwUpdate: 0
update_info.ucodeUpdate: 0
Client said: hello # クライアントから送信されたデータ
クライアント
code: bash
+ Init Enclave Successful 2! Running as client...
Entering ocall_sgx_init_quote
get_sigrl_from_intel
Report creation => success 131, 215, 25, 231, 125, 234, 202, 20, 112, 246, 186, 246, 42, 77, 119, 67, 3, 200, 153, 219, 105, 2, 15, 156, 112, 238, 29, 252, 8, 199, 206, 158 rand finished
Entering ocall_get_quote
quote size = 1116
sgx_calc_quote_size returned SGX_SUCCESS.
rsgx_verify_report passed!
post_report_from_intel
--received-server cert:
Time = 2021-09-30T12:45:55.086749+0000
rt=SGX_ERROR_UPDATE_NEEDED
update_info.pswUpdate: 1
update_info.csmeFwUpdate: 0
update_info.ucodeUpdate: 0
Server replied: hello back # サーバからのレスポンス
まとめ
データのシーリングはマシン依存するため、Enclaveアプリをスーケルさせるにはちょっと使い勝手が悪い
mutual-TLS通信を使えばEnclaveアプリ間でデータのやりとりができる
mTLS通信で使用するサーバ/クライアント証明書には認証局の証明書ではなくRemote AttestationのAttestationレポートを使う
地味にサーバ側のGraceful Shutdownの処理が大変だった
actix-web使えたら楽なんだけど、Enclave側で使えないので仕方なし
mutual-TLSの証明書にAttestationレポートを使うというのはとてもいいアイデアだなと感じた
お恥ずかしい話ですが、Anonifyに関わるまでmutual-TLSという言葉自体知りませんでした。過去にIoTの技術に関わったときにmTLS使ってるはずなんですが。。。
TLSに関して、バックエンドエンジニアとして普段のAPI開発でTLS通信(HTTPS)自体は高頻度で使用しているものの、ほとんどがL7のロードバランサを終端にして処理を任せていたのでTLSのプログラムを書くことなんでありませんでした。今回、TLSプログラミングを初めて書くことになり、はじめはTLSについての全然知識が浅くかなり苦戦しました。そのおかげでTLS自体の仕組みを深く勉強することができてとてもよい体験になりました。今回でAnonify解体新書のTEE編は一旦終わりにして次回からはAnonifyで使用しているブロックチェーン技術について解説したいと思います。(文責・藤田) Anonify解体新書 | 連載一覧(全8回)